//  Copyright (c) Microsoft Corporation.  All Rights Reserved.

namespace Common
{
    using System;
    using System.Xml;
    using System.IO;
    using System.Globalization;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Security.Cryptography;
    using System.Runtime.Serialization;
    using System.IdentityModel.Policy;
    using System.IdentityModel.Selectors;
    using System.IdentityModel.Tokens;
    using System.ServiceModel;
    using System.ServiceModel.Security;
    using System.ServiceModel.Security.Tokens;
    using System.ServiceModel.Channels;

    class TrustDriver
    {
        SecurityTokenSerializer tokenSerializer;

        public TrustDriver(SecurityTokenSerializer tokenSerializer)
        {
            if (tokenSerializer == null)
            {
                throw new ArgumentNullException("tokenSerializer");
            }
            this.tokenSerializer = tokenSerializer;
        }

        #region RequestSecurityToken
        public RequestSecurityToken CreateRequestSecurityToken(XmlReader xmlReader)
        {
            XmlDictionaryReader reader = XmlDictionaryReader.CreateDictionaryReader(xmlReader);
            reader.MoveToStartElement(WSTrust.RequestSecurityToken, WSTrust.Namespace);
            string context = null;
            string tokenTypeUri = null;
            string requestType = null;
            int keySize = 0;
            XmlDocument doc = new XmlDocument();
            XmlElement rstXml = (doc.ReadNode(reader) as XmlElement);
            SecurityKeyIdentifierClause renewTarget = null;
            SecurityKeyIdentifierClause closeTarget = null;
            for (int i = 0; i < rstXml.Attributes.Count; ++i)
            {
                XmlAttribute attr = rstXml.Attributes[i];
                if (attr.LocalName == WSTrust.Context)
                {
                    context = attr.Value;
                }
            }
            for (int i = 0; i < rstXml.ChildNodes.Count; ++i)
            {
                XmlElement child = (rstXml.ChildNodes[i] as XmlElement);
                if (child != null)
                {
                    if (child.LocalName == WSTrust.TokenType && child.NamespaceURI == WSTrust.Namespace)
                        tokenTypeUri = ReadTextElementAsTrimmedString(child);
                    else if (child.LocalName == WSTrust.RequestType && child.NamespaceURI == WSTrust.Namespace)
                        requestType = ReadTextElementAsTrimmedString(child);
                    else if (child.LocalName == WSTrust.KeySize && child.NamespaceURI == WSTrust.Namespace)
                        keySize = Int32.Parse(ReadTextElementAsTrimmedString(child), NumberFormatInfo.InvariantInfo);
                }
            }

            ReadTargets(rstXml, out renewTarget, out closeTarget);

            RequestSecurityToken rst = new RequestSecurityToken(this, rstXml, context, tokenTypeUri, requestType, keySize, renewTarget, closeTarget);
            return rst;
        }

        void ReadTargets(XmlElement rstXml, out SecurityKeyIdentifierClause renewTarget, out SecurityKeyIdentifierClause closeTarget)
        {
            renewTarget = null;
            closeTarget = null;

            for (int i = 0; i < rstXml.ChildNodes.Count; ++i)
            {
                XmlElement child = (rstXml.ChildNodes[i] as XmlElement);
                if (child != null)
                {
                    if (child.LocalName == WSTrust.RenewTarget && child.NamespaceURI == WSTrust.Namespace)
                        renewTarget = this.tokenSerializer.ReadKeyIdentifierClause(new XmlNodeReader(child.FirstChild));
                    else if (child.LocalName == WSTrust.CloseTarget && child.NamespaceURI == WSTrust.Namespace)
                        closeTarget = this.tokenSerializer.ReadKeyIdentifierClause(new XmlNodeReader(child.FirstChild));
                }
            }
        }

        public void WriteRequestSecurityToken(RequestSecurityToken rst, XmlWriter xmlWriter)
        {
            if (rst == null)
            {
                throw new ArgumentNullException("rst");
            }
            if (xmlWriter == null)
            {
                throw new ArgumentNullException("xmlWriter");
            }
            XmlDictionaryWriter writer = XmlDictionaryWriter.CreateDictionaryWriter(xmlWriter);
            if (rst.IsReceiver)
            {
                rst.WriteTo(writer);
                return;
            }
            writer.WriteStartElement(WSTrust.Prefix, WSTrust.RequestSecurityToken, WSTrust.Namespace);
            if (rst.Context != null)
                writer.WriteAttributeString(WSTrust.Context, null, rst.Context);

            rst.OnWriteCustomAttributes(writer);
            if (rst.TokenType != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.TokenType, WSTrust.Namespace);
                writer.WriteString(rst.TokenType);
                writer.WriteEndElement();
            }
            if (rst.RequestType != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.RequestType, WSTrust.Namespace);
                writer.WriteString(rst.RequestType);
                writer.WriteEndElement();
            }

            if (rst.AppliesTo != null)
            {
                WriteAppliesTo(rst.AppliesTo, rst.AppliesToType, rst.AppliesToSerializer, writer);
            }

            SecurityToken entropyToken = rst.GetRequestorEntropy();
            if (entropyToken != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.Entropy, WSTrust.Namespace);
                this.tokenSerializer.WriteToken(writer, entropyToken);
                writer.WriteEndElement();
            }

            if (rst.KeySize != 0)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.KeySize, WSTrust.Namespace);
                writer.WriteValue(rst.KeySize);
                writer.WriteEndElement();
            }

            WriteTargets(rst, writer);

            if (rst.RequestProperties != null)
            {
                foreach (XmlElement property in rst.RequestProperties)
                {
                    property.WriteTo(writer);
                }
            }

            rst.OnWriteCustomElements(writer);
            writer.WriteEndElement();
        }

        void WriteTargets(RequestSecurityToken rst, XmlDictionaryWriter writer)
        {
            if (rst.RenewTarget != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.RenewTarget, WSTrust.Namespace);
                this.tokenSerializer.WriteKeyIdentifierClause(writer, rst.RenewTarget);
                writer.WriteEndElement();
            }

            if (rst.CloseTarget != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.CloseTarget, WSTrust.Namespace);
                this.tokenSerializer.WriteKeyIdentifierClause(writer, rst.CloseTarget);
                writer.WriteEndElement();
            }
        }

        public bool IsRequestedSecurityTokenElement(string name, string nameSpace)
        {
            return (name == WSTrust.RequestedSecurityToken && nameSpace == WSTrust.Namespace);
        }
        #endregion

        #region AppliesTo
        XmlElement GetAppliesToElement(XmlElement rootElement)
        {
            if (rootElement == null)
            {
                return null;
            }
            for (int i = 0; i < rootElement.ChildNodes.Count; ++i)
            {
                XmlElement elem = (rootElement.ChildNodes[i] as XmlElement);
                if (elem != null)
                {
                    if (elem.LocalName == WSTrust.AppliesTo && elem.NamespaceURI == WSPolicy.Namespace)
                    {
                        return elem;
                    }
                }
            }
            return null;
        }

        T GetAppliesTo<T>(XmlElement rootXml, XmlObjectSerializer serializer)
        {
            XmlElement appliesToElement = GetAppliesToElement(rootXml);
            if (appliesToElement != null)
            {
                using (XmlReader reader = new XmlNodeReader(appliesToElement))
                {
                    reader.ReadStartElement();
                    lock (serializer)
                    {
                        return (T)serializer.ReadObject(reader);
                    }
                }
            }
            else
            {
                throw new InvalidOperationException("AppliesTo element was not present");
            }
        }

        public T GetAppliesTo<T>(RequestSecurityToken rst, XmlObjectSerializer serializer)
        {
            if (rst == null)
                throw new ArgumentNullException("rst");

            return GetAppliesTo<T>(rst.RequestSecurityTokenXml, serializer);
        }


        public T GetAppliesTo<T>(RequestSecurityTokenResponse rstr, XmlObjectSerializer serializer)
        {
            if (rstr == null)
                throw new ArgumentNullException("rstr");

            return GetAppliesTo<T>(rstr.RequestSecurityTokenResponseXml, serializer);
        }

        public void GetAppliesToQName(XmlElement rootElement, out string localName, out string namespaceUri)
        {
            localName = namespaceUri = null;
            XmlElement appliesToElement = GetAppliesToElement(rootElement);
            if (appliesToElement != null)
            {
                using (XmlReader reader = new XmlNodeReader(appliesToElement))
                {
                    reader.ReadStartElement();
                    reader.MoveToContent();
                    localName = reader.LocalName;
                    namespaceUri = reader.NamespaceURI;
                }
            }
        }

        public void GetAppliesToQName(RequestSecurityToken rst, out string localName, out string namespaceUri)
        {
            if (rst == null)
                throw new ArgumentNullException("rst");

            GetAppliesToQName(rst.RequestSecurityTokenXml, out localName, out namespaceUri);
        }

        void WriteAppliesTo(object appliesTo, Type appliesToType, XmlObjectSerializer serializer, XmlWriter xmlWriter)
        {
            XmlDictionaryWriter writer = XmlDictionaryWriter.CreateDictionaryWriter(xmlWriter);
            writer.WriteStartElement(WSPolicy.Prefix, WSTrust.AppliesTo, WSPolicy.Namespace);
            lock (serializer)
            {
                serializer.WriteObject(writer, appliesTo);
            }
            writer.WriteEndElement();
        }
        #endregion

        #region Entropy
        public SecurityToken GetEntropy(RequestSecurityToken rst, SecurityTokenResolver resolver)
        {
            if (rst == null)
                throw new ArgumentNullException("rst");

            return GetEntropy(rst.RequestSecurityTokenXml, resolver);
        }

        public void GetAppliesToQName(RequestSecurityTokenResponse rstr, out string localName, out string namespaceUri)
        {
            if (rstr == null)
                throw new ArgumentNullException("rstr");

            GetAppliesToQName(rstr.RequestSecurityTokenResponseXml, out localName, out namespaceUri);
        }

        SecurityToken GetEntropy(XmlElement rootElement, SecurityTokenResolver resolver)
        {
            if (rootElement == null || rootElement.ChildNodes == null)
            {
                return null;
            }
            for (int i = 0; i < rootElement.ChildNodes.Count; ++i)
            {
                XmlElement element = rootElement.ChildNodes[i] as XmlElement;
                if (element != null)
                {
                    if (element.LocalName == WSTrust.Entropy && element.NamespaceURI == WSTrust.Namespace)
                    {
                        XmlElement tokenXml = GetChildElement(element);
                        string valueTypeUri = element.GetAttribute("ValueType");
                        if (valueTypeUri.Length == 0)
                            valueTypeUri = null;
                        return this.tokenSerializer.ReadToken(new XmlNodeReader(tokenXml), resolver);
                    }
                }
            }
            return null;
        }

        #endregion

        #region RequestSecurityTokenResponse


        public RequestSecurityTokenResponse CreateRequestSecurityTokenResponse(XmlReader xmlReader)
        {
            if (xmlReader == null)
            {
                throw new ArgumentNullException("xmlReader");
            }
            XmlDictionaryReader reader = XmlDictionaryReader.CreateDictionaryReader(xmlReader);
            if (reader.IsStartElement(WSTrust.RequestSecurityTokenResponse, WSTrust.Namespace) == false)
            {
                throw new XmlException(String.Format("Required Element ({0}, {1}) missing", WSTrust.RequestSecurityTokenResponse, WSTrust.Namespace));
            }

            XmlDocument doc = new XmlDocument();
            XmlElement rstrXml = doc.ReadNode(reader) as XmlElement;

            string context = null;
            string tokenTypeUri = null;
            int keySize = 0;
            SecurityKeyIdentifierClause requestedAttachedReference = null;
            SecurityKeyIdentifierClause requestedUnattachedReference = null;
            bool computeKey = false;
            DateTime created = DateTime.UtcNow;
            DateTime expires = new DateTime(DateTime.MaxValue.Ticks - TimeSpan.FromDays(2).Ticks, DateTimeKind.Utc);
            bool isRequestedTokenClosed = false;
            for (int i = 0; i < rstrXml.Attributes.Count; ++i)
            {
                XmlAttribute attr = rstrXml.Attributes[i];
                if (attr.LocalName == WSTrust.Context)
                {
                    context = attr.Value;
                }
            }

            for (int i = 0; i < rstrXml.ChildNodes.Count; ++i)
            {
                XmlElement child = (rstrXml.ChildNodes[i] as XmlElement);
                if (child != null)
                {
                    if (child.LocalName == WSTrust.TokenType && child.NamespaceURI == WSTrust.Namespace)
                        tokenTypeUri = ReadTextElementAsTrimmedString(child);
                    else if (child.LocalName == WSTrust.KeySize && child.NamespaceURI == WSTrust.Namespace)
                        keySize = Int32.Parse(ReadTextElementAsTrimmedString(child), NumberFormatInfo.InvariantInfo);
                    else if (child.LocalName == WSTrust.RequestedProofToken && child.NamespaceURI == WSTrust.Namespace)
                    {
                        XmlElement proofXml = GetChildElement(child);
                        if (proofXml.LocalName == WSTrust.ComputedKey && proofXml.NamespaceURI == WSTrust.Namespace)
                        {
                            string computedKeyAlgorithm = ReadTextElementAsTrimmedString(proofXml);
                            if (computedKeyAlgorithm != WSTrust.Psha1ComputedKeyUri)
                            {
                                throw new MessageSecurityException(String.Format("Unknown computed key algorithm {0}", computedKeyAlgorithm));
                            }
                            computeKey = true;
                        }
                    }
                    else if (child.LocalName == WSTrust.Lifetime && child.NamespaceURI == WSTrust.Namespace)
                    {
                        XmlElement createdXml = GetChildElement(child, WSUtility.Created, WSUtility.Namespace);
                        if (createdXml != null)
                        {
                            created = DateTime.ParseExact(ReadTextElementAsTrimmedString(createdXml),
                                WSUtility.AcceptedDateTimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None).ToUniversalTime();
                        }
                        XmlElement expiresXml = GetChildElement(child, WSUtility.Expires, WSUtility.Namespace);
                        if (expiresXml != null)
                        {
                            expires = DateTime.ParseExact(ReadTextElementAsTrimmedString(expiresXml),
                                WSUtility.AcceptedDateTimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None).ToUniversalTime();
                        }
                    }
                }
            }

            isRequestedTokenClosed = ReadRequestedTokenClosed(rstrXml);
            ReadReferences(rstrXml, out requestedAttachedReference, out requestedUnattachedReference);

            return new RequestSecurityTokenResponse(this, rstrXml, context, tokenTypeUri, keySize, requestedAttachedReference, requestedUnattachedReference,
                                                    computeKey, created, expires, isRequestedTokenClosed);
        }


        void ReadReferences(XmlElement rstrXml, out SecurityKeyIdentifierClause requestedAttachedReference, out SecurityKeyIdentifierClause requestedUnattachedReference)
        {
            XmlElement issuedTokenXml = null;
            requestedAttachedReference = null;
            requestedUnattachedReference = null;
            for (int i = 0; i < rstrXml.ChildNodes.Count; ++i)
            {
                XmlElement child = rstrXml.ChildNodes[i] as XmlElement;
                if (child != null)
                {
                    if (child.LocalName == WSTrust.RequestedSecurityToken && child.NamespaceURI == WSTrust.Namespace)
                    {
                        issuedTokenXml = GetChildElement(child);
                    }
                    else if (child.LocalName == WSTrust.RequestedAttachedReference && child.NamespaceURI == WSTrust.Namespace)
                    {
                        requestedAttachedReference = this.tokenSerializer.ReadKeyIdentifierClause(new XmlNodeReader(GetChildElement(child)));
                    }
                    else if (child.LocalName == WSTrust.RequestedUnattachedReference && child.NamespaceURI == WSTrust.Namespace)
                    {
                        requestedUnattachedReference = this.tokenSerializer.ReadKeyIdentifierClause(new XmlNodeReader(GetChildElement(child)));
                    }
                }
            }

            try
            {
                if (issuedTokenXml != null)
                {
                    WSSecurityTokenSerializer innerTokenSerializer = ((WSTrustTokenSerializer)this.tokenSerializer).InnerTokenSerializer as WSSecurityTokenSerializer;
                    if (innerTokenSerializer != null)
                    {
                        if (requestedAttachedReference == null)
                        {
                            requestedAttachedReference = innerTokenSerializer.CreateKeyIdentifierClauseFromTokenXml(issuedTokenXml, SecurityTokenReferenceStyle.Internal);
                        }
                        if (requestedUnattachedReference == null)
                        {
                            requestedUnattachedReference = innerTokenSerializer.CreateKeyIdentifierClauseFromTokenXml(issuedTokenXml, SecurityTokenReferenceStyle.External);
                        }
                    }
                }
            }
            catch (XmlException)
            {
                throw new XmlException("Unable to determine token reference from token XML");
            }

        }

        static bool ReadRequestedTokenClosed(XmlElement rstrXml)
        {
            for (int i = 0; i < rstrXml.ChildNodes.Count; ++i)
            {
                XmlElement child = (rstrXml.ChildNodes[i] as XmlElement);
                if (child != null)
                {
                    if (child.LocalName == WSTrust.RequestedTokenClosed && child.NamespaceURI == WSTrust.Namespace)
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        public SecurityToken GetEntropy(RequestSecurityTokenResponse rstr, SecurityTokenResolver resolver)
        {
            if (rstr == null)
                throw new ArgumentNullException("rstr");

            return GetEntropy(rstr.RequestSecurityTokenResponseXml, resolver);
        }

        public void WriteRequestSecurityTokenResponse(RequestSecurityTokenResponse rstr, XmlWriter xmlWriter)
        {
            if (rstr == null)
                throw new ArgumentNullException("rstr");
            if (xmlWriter == null)
                throw new ArgumentNullException("xmlWriter");
            XmlDictionaryWriter writer = XmlDictionaryWriter.CreateDictionaryWriter(xmlWriter);
            if (rstr.IsReceiver)
            {
                rstr.WriteTo(writer);
                return;
            }
            writer.WriteStartElement(WSTrust.Prefix, WSTrust.RequestSecurityTokenResponse, WSTrust.Namespace);
            if (rstr.Context != null)
            {
                writer.WriteAttributeString(WSTrust.Context, null, rstr.Context);
            }
            rstr.OnWriteCustomAttributes(writer);

            if (rstr.TokenType != null)
                writer.WriteElementString(WSTrust.Prefix, WSTrust.TokenType, WSTrust.Namespace, rstr.TokenType);

            if (rstr.RequestedSecurityToken != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.RequestedSecurityToken, WSTrust.Namespace);
                tokenSerializer.WriteToken(writer, rstr.RequestedSecurityToken);
                writer.WriteEndElement();
            }

            if (rstr.AppliesTo != null)
            {
                WriteAppliesTo(rstr.AppliesTo, rstr.AppliesToType, rstr.AppliesToSerializer, writer);
            }

            WriteReferences(rstr, writer);

            if (rstr.ComputeKey || rstr.RequestedProofToken != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.RequestedProofToken, WSTrust.Namespace);
                if (rstr.ComputeKey)
                {
                    writer.WriteElementString(WSTrust.Prefix, WSTrust.ComputedKey, WSTrust.Namespace, WSTrust.Psha1ComputedKeyUri);
                }
                else
                {
                    tokenSerializer.WriteToken(writer, rstr.RequestedProofToken);
                }
                writer.WriteEndElement();
            }

            SecurityToken entropyToken = rstr.GetIssuerEntropy();
            if (entropyToken != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.Entropy, WSTrust.Namespace);
                tokenSerializer.WriteToken(writer, entropyToken);
                writer.WriteEndElement();
            }

            // To write out the lifetime, the following algorithm is used
            //   1. If the lifetime is explicitly set, write it out.
            //   2. Else, if a token/tokenbuilder has been set, use the lifetime in that.
            //   3. Else do not serialize lifetime
            if (rstr.IsLifetimeSet || rstr.RequestedSecurityToken != null)
            {
                DateTime effectiveTime = DateTime.UtcNow;
                DateTime expirationTime = new DateTime(DateTime.MaxValue.Ticks - TimeSpan.FromDays(2).Ticks, DateTimeKind.Utc);

                if (rstr.IsLifetimeSet)
                {
                    effectiveTime = rstr.ValidFrom.ToUniversalTime();
                    expirationTime = rstr.ValidTo.ToUniversalTime();
                }
                else if (rstr.RequestedSecurityToken != null)
                {
                    effectiveTime = rstr.RequestedSecurityToken.ValidFrom.ToUniversalTime();
                    expirationTime = rstr.RequestedSecurityToken.ValidTo.ToUniversalTime();
                }

                // write out the lifetime
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.Lifetime, WSTrust.Namespace);
                // write out Created
                writer.WriteStartElement(WSUtility.Prefix, WSUtility.Created, WSUtility.Namespace);
                writer.WriteString(effectiveTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture.DateTimeFormat));
                writer.WriteEndElement(); // wsu:Created
                // write out Expires
                writer.WriteStartElement(WSUtility.Prefix, WSUtility.Expires, WSUtility.Namespace);
                writer.WriteString(expirationTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture.DateTimeFormat));
                writer.WriteEndElement(); // wsu:Expires
                writer.WriteEndElement(); // wsse:Lifetime
            }

            if (rstr.KeySize > 0)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.KeySize, WSTrust.Namespace);
                writer.WriteValue(rstr.KeySize);
                writer.WriteEndElement();
            }

            WriteRequestedTokenClosed(rstr, writer);

            rstr.OnWriteCustomElements(writer);
            writer.WriteEndElement();
        }

        void WriteReferences(RequestSecurityTokenResponse rstr, XmlDictionaryWriter writer)
        {
            if (rstr.RequestedAttachedReference != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.RequestedAttachedReference, WSTrust.Namespace);
                this.tokenSerializer.WriteKeyIdentifierClause(writer, rstr.RequestedAttachedReference);
                writer.WriteEndElement();
            }

            if (rstr.RequestedUnattachedReference != null)
            {
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.RequestedUnattachedReference, WSTrust.Namespace);
                this.tokenSerializer.WriteKeyIdentifierClause(writer, rstr.RequestedUnattachedReference);
                writer.WriteEndElement();
            }
        }

        static void WriteRequestedTokenClosed(RequestSecurityTokenResponse rstr, XmlDictionaryWriter writer)
        {
            if (rstr.IsRequestedTokenClosed)
            {
                writer.WriteElementString(WSTrust.RequestedTokenClosed, WSTrust.Namespace, String.Empty);
            }
        }

        static bool IsAtRequestSecurityTokenResponse(XmlReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");

            return reader.IsStartElement(WSTrust.RequestSecurityTokenResponse, WSTrust.Namespace);
        }

        static bool IsRequestedProofTokenElement(string name, string nameSpace)
        {
            return (name == WSTrust.RequestedProofToken && nameSpace == WSTrust.Namespace);
        }

        #endregion

        #region GetIssuedToken
        static void ValidateRequestorEntropy(SecurityToken entropy, SecurityKeyEntropyMode mode)
        {
            if ((mode == SecurityKeyEntropyMode.ClientEntropy || mode == SecurityKeyEntropyMode.CombinedEntropy)
                && (entropy == null))
            {
                throw new InvalidOperationException(String.Format("Entropy mode {0} requires the client entropy to be specified", mode));
            }
            if (mode == SecurityKeyEntropyMode.ServerEntropy && entropy != null)
            {
                throw new InvalidOperationException(String.Format("Entropy mode {0} does not allow the client entropy to be specified", mode));
            }
        }

        internal static void ValidateRequestedKeySize(int keySize, SecurityAlgorithmSuite algorithmSuite)
        {
            if ((keySize % 8 == 0) && algorithmSuite.IsSymmetricKeyLengthSupported(keySize))
            {
                return;
            }
            else
            {
                throw new InvalidOperationException(String.Format("The keysize '{0}' specified is invalid", keySize));
            }
        }

        void GetIssuedAndProofXml(RequestSecurityTokenResponse rstr, out XmlElement issuedTokenXml, out XmlElement proofTokenXml)
        {
            issuedTokenXml = null;
            proofTokenXml = null;
            if ((rstr.RequestSecurityTokenResponseXml != null) && (rstr.RequestSecurityTokenResponseXml.ChildNodes != null))
            {
                for (int i = 0; i < rstr.RequestSecurityTokenResponseXml.ChildNodes.Count; ++i)
                {
                    XmlElement elem = rstr.RequestSecurityTokenResponseXml.ChildNodes[i] as XmlElement;
                    if (elem != null)
                    {
                        if (elem.LocalName == WSTrust.RequestedSecurityToken && elem.NamespaceURI == WSTrust.Namespace)
                        {
                            if (issuedTokenXml != null)
                            {
                                throw new InvalidOperationException("RequestSecurityTokenResponse has multiple RequestedSecurityToken elements");
                            }
                            issuedTokenXml = GetChildElement(elem);
                        }
                        else if (elem.LocalName == WSTrust.RequestedProofToken && elem.NamespaceURI == WSTrust.Namespace)
                        {
                            if (proofTokenXml != null)
                            {
                                throw new InvalidOperationException("RequestSecurityTokenResponse has multiple RequestedProofToken elements");
                            }
                            proofTokenXml = GetChildElement(elem);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// The algorithm for computing the key is:
        /// 1. If there is requestorEntropy:
        ///    a. If there is no <RequestedProofToken> use the requestorEntropy as the key
        ///    b. If there is a <RequestedProofToken> with a ComputedKeyUri, combine the client and server entropies
        ///    c. Anything else, throw
        /// 2. If there is no requestorEntropy:
        ///    a. THere has to be a <RequestedProofToken> that contains the proof key
        /// </summary>
        public GenericXmlSecurityToken GetIssuedToken(RequestSecurityTokenResponse rstr, SecurityTokenResolver resolver, IList<SecurityTokenAuthenticator> allowedAuthenticators, SecurityKeyEntropyMode keyEntropyMode, byte[] requestorEntropy, string expectedTokenType,
            ReadOnlyCollection<IAuthorizationPolicy> authorizationPolicies, int defaultKeySize)
        {
            if (defaultKeySize < 0)
            {
                throw new ArgumentOutOfRangeException("defaultKeySize");
            }

            if (rstr == null)
                throw new ArgumentNullException("rstr");

            string tokenType;
            if (rstr.TokenType != null)
            {
                if (expectedTokenType != null && expectedTokenType != rstr.TokenType)
                {
                    throw new InvalidOperationException(String.Format("Unexpected token type {0}, expected {1}", rstr.TokenType, expectedTokenType));
                }
                tokenType = rstr.TokenType;
            }
            else
            {
                tokenType = expectedTokenType;
            }

            // search the response elements for licenseXml, proofXml, and lifetime
            DateTime created = rstr.ValidFrom;
            DateTime expires = rstr.ValidTo;
            XmlElement proofXml;
            XmlElement issuedTokenXml;
            GetIssuedAndProofXml(rstr, out issuedTokenXml, out proofXml);

            if (issuedTokenXml == null)
                throw new InvalidOperationException("The RequestedSecurityToken element found");

            SecurityToken proofToken;
            SecurityToken entropyToken = GetEntropy(rstr, resolver);
            if (keyEntropyMode == SecurityKeyEntropyMode.ClientEntropy)
            {
                if (requestorEntropy == null)
                {
                    throw new InvalidOperationException(String.Format("Entropy mode '{0}' requires client entropy", keyEntropyMode));
                }
                // enforce that there is no entropy or proof token in the RSTR
                if (proofXml != null || entropyToken != null)
                {
                    throw new InvalidOperationException(String.Format("Entropy mode '{0}' cannot have server entropy or proof token", keyEntropyMode));
                }
                proofToken = new BinarySecretSecurityToken(requestorEntropy);
            }
            else if (keyEntropyMode == SecurityKeyEntropyMode.ServerEntropy)
            {
                if (requestorEntropy != null)
                {
                    throw new InvalidOperationException(String.Format("Entropy mode '{0}' cannot have client entropy", keyEntropyMode));
                }
                if (rstr.ComputeKey || entropyToken != null)
                {
                    throw new InvalidOperationException(String.Format("Entropy mode '{0}' cannot have computed key or server entropy", keyEntropyMode));
                }
                if (proofXml == null)
                {
                    throw new InvalidOperationException(String.Format("Entropy mode '{0}' requires proof token", keyEntropyMode));
                }
                string valueTypeUri = proofXml.GetAttribute("ValueType");
                if (valueTypeUri.Length == 0)
                    valueTypeUri = null;
                proofToken = tokenSerializer.ReadToken(new XmlNodeReader(proofXml), resolver);
            }
            else
            {
                if (!rstr.ComputeKey)
                {
                    throw new InvalidOperationException(String.Format("Entropy mode '{0}' requires computed key", keyEntropyMode));
                }
                if (entropyToken == null)
                {
                    throw new InvalidOperationException(String.Format("Entropy mode '{0}' requires server entropy", keyEntropyMode));
                }
                if (requestorEntropy == null)
                {
                    throw new InvalidOperationException(String.Format("Entropy mode '{0}' requires client entropy", keyEntropyMode));
                }
                if (rstr.KeySize == 0 && defaultKeySize == 0)
                {
                    throw new InvalidOperationException("The KeySize is not specified");
                }
                int issuedKeySize = (rstr.KeySize != 0) ? rstr.KeySize : defaultKeySize;
                byte[] issuerEntropy;
                if (entropyToken is BinarySecretSecurityToken)
                    issuerEntropy = ((BinarySecretSecurityToken)entropyToken).GetKeyBytes();
                else if (entropyToken is WrappedKeySecurityToken)
                    issuerEntropy = ((WrappedKeySecurityToken)entropyToken).GetWrappedKey();
                else
                    throw new NotSupportedException("Unsupported issuer entropy");
                // compute the PSHA1 derived key
                Psha1DerivedKeyGenerator keyGenerator = new Psha1DerivedKeyGenerator(requestorEntropy);
                byte[] issuedKey = keyGenerator.GenerateDerivedKey(new byte[] { }, issuerEntropy, issuedKeySize, 0);
                proofToken = new BinarySecretSecurityToken(issuedKey);
            }

            SecurityKeyIdentifierClause internalReference = rstr.RequestedAttachedReference;
            SecurityKeyIdentifierClause externalReference = rstr.RequestedUnattachedReference;

            return new GenericXmlSecurityToken(issuedTokenXml, proofToken, created, expires, internalReference, externalReference, authorizationPolicies);
        }

        public GenericXmlSecurityToken GetIssuedToken(RequestSecurityTokenResponse rstr, string expectedTokenType,
            ReadOnlyCollection<IAuthorizationPolicy> authorizationPolicies, RSA clientKey)
        {
            if (rstr == null)
                throw new ArgumentNullException("rstr");

            string tokenType;
            if (rstr.TokenType != null)
            {
                if (expectedTokenType != null && expectedTokenType != rstr.TokenType)
                {
                    throw new InvalidOperationException(String.Format("Unexpected token type {0}, expected {1}", rstr.TokenType, expectedTokenType));
                }
                tokenType = rstr.TokenType;
            }
            else
            {
                tokenType = expectedTokenType;
            }

            // search the response elements for licenseXml, proofXml, and lifetime
            DateTime created = rstr.ValidFrom;
            DateTime expires = rstr.ValidTo;
            XmlElement proofXml;
            XmlElement issuedTokenXml;
            GetIssuedAndProofXml(rstr, out issuedTokenXml, out proofXml);

            if (issuedTokenXml == null)
                throw new InvalidOperationException("RequestedSecurityToken element not present");

            // enforce that there is no proof token in the RSTR
            if (proofXml != null)
            {
                throw new InvalidOperationException("RequestedProofToken element not expected");
            }
            SecurityKeyIdentifierClause internalReference = rstr.RequestedAttachedReference;
            SecurityKeyIdentifierClause externalReference = rstr.RequestedUnattachedReference;

            SecurityToken proofToken = new RsaSecurityToken(clientKey);
            return new GenericXmlSecurityToken(issuedTokenXml, proofToken, created, expires, internalReference, externalReference, authorizationPolicies);
        }
        #endregion

        #region XML utility methods
        static bool IsWhitespace(char ch)
        {
            return (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n');
        }

        static string ReadTextElementAsTrimmedString(XmlElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException("element");
            }

            using (XmlReader reader = new XmlNodeReader(element))
            {
                reader.MoveToContent();
                string s = reader.ReadElementContentAsString();
                int i;
                for (i = 0; i < s.Length && IsWhitespace(s[i]); i++) ;
                if (i >= s.Length)
                {
                    return string.Empty;
                }

                int j;
                for (j = s.Length; j > 0 && IsWhitespace(s[j - 1]); j--) ;

                if (i != 0 || j != s.Length)
                {
                    return s.Substring(i, j - i);
                }
                return s;
            }
        }

        static XmlElement GetChildElement(XmlElement parent)
        {
            if (parent == null)
            {
                throw new ArgumentNullException("parent");
            }

            XmlElement result = null;
            for (int i = 0; i < parent.ChildNodes.Count; ++i)
            {
                XmlNode child = parent.ChildNodes[i];
                if (child.NodeType == XmlNodeType.Whitespace || child.NodeType == XmlNodeType.Comment)
                {
                    continue;
                }
                else if (child.NodeType == XmlNodeType.Element && result == null)
                {
                    result = ((XmlElement)child);
                }
                else
                {
                    throw new XmlException("Unexpected child node");
                }
            }

            if (result == null)
            {
                throw new XmlException("child element missing");
            }

            return result;
        }

        static XmlElement GetChildElement(XmlElement parent, string childLocalName, string childNamespace)
        {
            if (parent == null)
            {
                throw new ArgumentNullException("parent");
            }

            for (int i = 0; i < parent.ChildNodes.Count; ++i)
            {
                XmlNode child = parent.ChildNodes[i];

                if (child.NodeType == XmlNodeType.Whitespace || child.NodeType == XmlNodeType.Comment)
                {
                    continue;
                }
                else if (child.NodeType == XmlNodeType.Element)
                {
                    if (child.LocalName == childLocalName && child.NamespaceURI == childNamespace)
                    {
                        return ((XmlElement)child);
                    }
                }
                else
                {
                    throw new XmlException("Unexpected child node");
                }
            }

            return null;
        }
        #endregion
    }

    class WSTrustTokenSerializer : SecurityTokenSerializer
    {
        SecurityTokenSerializer innerTokenSerializer;

        public WSTrustTokenSerializer(SecurityTokenSerializer innerTokenSerializer)
        {
            if (innerTokenSerializer == null)
            {
                throw new ArgumentNullException("innerTokenSerializer");
            }
            this.innerTokenSerializer = innerTokenSerializer;
        }

        public SecurityTokenSerializer InnerTokenSerializer
        {
            get { return this.innerTokenSerializer; }
        }

        protected override bool CanReadKeyIdentifierClauseCore(XmlReader reader)
        {
            return this.innerTokenSerializer.CanReadKeyIdentifierClause(reader);
        }

        protected override bool CanReadKeyIdentifierCore(XmlReader reader)
        {
            return this.innerTokenSerializer.CanReadKeyIdentifier(reader);
        }

        protected override bool CanReadTokenCore(XmlReader reader)
        {
            return this.innerTokenSerializer.CanReadToken(reader);
        }

        protected override bool CanWriteKeyIdentifierClauseCore(SecurityKeyIdentifierClause keyIdentifierClause)
        {
            return this.innerTokenSerializer.CanWriteKeyIdentifierClause(keyIdentifierClause);
        }

        protected override bool CanWriteKeyIdentifierCore(SecurityKeyIdentifier keyIdentifier)
        {
            return this.innerTokenSerializer.CanWriteKeyIdentifier(keyIdentifier);
        }

        protected override bool CanWriteTokenCore(SecurityToken token)
        {
            return ((token is NonceToken) || this.innerTokenSerializer.CanWriteToken(token));
        }

        protected override SecurityKeyIdentifierClause ReadKeyIdentifierClauseCore(XmlReader reader)
        {
            return this.innerTokenSerializer.ReadKeyIdentifierClause(reader);
        }

        protected override SecurityKeyIdentifier ReadKeyIdentifierCore(XmlReader reader)
        {
            return this.innerTokenSerializer.ReadKeyIdentifier(reader);
        }

        protected override SecurityToken ReadTokenCore(XmlReader reader, SecurityTokenResolver tokenResolver)
        {
            return this.innerTokenSerializer.ReadToken(reader, tokenResolver);
        }

        protected override void WriteKeyIdentifierClauseCore(XmlWriter writer, SecurityKeyIdentifierClause keyIdentifierClause)
        {
            this.innerTokenSerializer.WriteKeyIdentifierClause(writer, keyIdentifierClause);
        }

        protected override void WriteKeyIdentifierCore(XmlWriter writer, SecurityKeyIdentifier keyIdentifier)
        {
            this.innerTokenSerializer.WriteKeyIdentifier(writer, keyIdentifier);
        }

        protected override void WriteTokenCore(XmlWriter writer, SecurityToken token)
        {
            if (token is NonceToken)
            {
                NonceToken nonce = (NonceToken)token;
                byte[] secret = nonce.GetKeyBytes();
                writer.WriteStartElement(WSTrust.Prefix, WSTrust.BinarySecret, WSTrust.Namespace);
                writer.WriteAttributeString("Type", null, WSTrust.NonceType);
                writer.WriteBase64(secret, 0, secret.Length);
                writer.WriteEndElement();
            }
            else
            {
                this.innerTokenSerializer.WriteToken(writer, token);
            }
        }
    }

    class NonceToken : BinarySecretSecurityToken
    {
        public NonceToken(byte[] key)
            : base(key)
        {
        }
    }
}
